RISC-V: A Baremetal Introduction using C++. Development Environment
Following on from Part 2, how do we compile this project and run it?
For this series of posts, my platform is a SiFive HiFive1 Rev B development board. It’s equipped with a 320MHz RV32IMAC (FE310 core). For software build and debug the choice is Platform IO, an IDE integrated into VS Code.
Setup
To setup a new project in Platform IO just select the board, framework, and path for the files. For this exercise, the choices are HiFive1 Rev B, Freedom E SDK, and a custom path. Customization can be done via the platformio.ini
file. We need to do this to configure our C++ version and baremetal environment.
In this example platformio.ini
's build_flags
has been extended with options to target embedded C++17. In particular:
-nostartfiles
is used as this example includes a custom startup routine,-std=c++17
for all the needed modern C++ features, and-fno-threadsafe-statics
to prevent threadsafe code from being emitted (we have no threads and these ensure static declarations are initialized only once).
The configuration build_flags
captures compiler, pre-processor, and linker options, so we can add -Wl,-Map,blinky.map
to generate a ma here alongside compiler options.
The complete set of build flags used is here:
build_flags =
-std=c++17
-O2
-g
-Wall
-ffunction-sections
-fno-exceptions
-fno-rtti
-fno-nonansi-builtins
-fno-use-cxa-atexit
-fno-threadsafe-statics
-nostartfiles
-Wl,-Map,blinky.map
That configuration is all we need to build this project.
Adding a Post Compile Action
For this exercise, we’ll want to check exactly how the compiler is transforming our C++ code. Disassembling the output will show us that.
How can Platform IO do this? We need to edit platform.ini
again, this time adding a helper script and additional targets.
extra_scripts = post_build.py
targets = disasm
The build system behind Platform IO is Scons. This is a Python-based system, so the post build script is a python file.
The Scons environment is used to find the path to the output file, ${BUILD_DIR}/${PROGNAME}.elf
, and the toolchain objdump is found by modifying the path to${OBJDUMP}
. The env.subst()
will expand the environment variables, and env.Execute()
will run the command.
def after_build(source, target, env):
""" Run objdump on the target elf file and save the
output in the top dir.
"""
objdump=env.subst("${OBJCOPY}").replace("objcopy","objdump")
src_elf=env.subst("${BUILD_DIR}/${PROGNAME}.elf")
cmd=" ".join([
objdump, "-SC","--file-start-context", "-w",
src_elf,">","${PROGNAME}.disasm"]) #
env.Execute(cmd)
The target is given a name disasm
via theenv.AddCustomTarget()
method. This ties into the platformio.ini
option targets = disasm
above.
Import("env")env.AddCustomTarget(
"disasm",
"${BUILD_DIR}/${PROGNAME}.elf",
after_build,
title="Disasm 2",
description="Generate a disassembly file on demand",
always_build=True
)
The above is all that is needed to compile the project. Loading it is simple, Platform IO takes care of it.
The project for this example is here. You should be able to load this project with Platform IO and try it out. With some small modifications, it should work on any other toolchain, IDE, or target.
The next post looks at how the core starts from reset and executes the program.
The rest of this post will look at other development environments and target platform options.
Other C++ Development Environments
PlaformIO as it’s a great way to get started with no messing around, but let’s explore a few other options that are available.
The standard toolchain for RISC-V is GCC, although LLVM RISC-V support is also available. Platform IO will automatically install SiFive’s toolchain and SDK.
If you are like most developers, an IDE may be your choice.
- SiFive, like most processor vendors, has a GCC and Eclipse-based IDE, the Freedom Studio.
- For a vendor-independent path GNU MCU Eclipse supports RISC-V compilation and debugging very well. I have personally used this to target proprietary RISC-V cores and simulators with success.
However, if like me your usual preference is command-line oriented, then Make or CMake may be your choice.
- The toolchain binaries used by GNU MCU Eclipse can be installed independently via xpack riscv-none-embed-gcc.
- CMake with a RISC-V toolchain is possible.
- For complete toolchain control, you can compile GCC and it’s supporting libraries via the github riscv-gnu-toolchain project.
Compiling your own GCC is useful for those looking for bleeding-edge extension support, or to change variant that the default C libraries are compiled for. (For example, In the past I have used this option to target an RV32EC core before pre-compiled binaries were available.)
The IDEs above all use OpenOCD for debugging support, and this can be downloaded and compiled from OpenOCD source on GitHub, or binaries are available from xpack.
A complete list is maintained by the RISC-V org.
Other RISC-V Devices
These posts target SiFive’s HiFive1 Rev B as it’s easily available and easy to use. However, the point of programming RISC-V at this low level is to make use of the open architecture to build bigger custom systems. As I’ve been on the firmware side this list is not complete, but just a starting point:
The main IP vendors seem to be:
- SiFive. SiFive has a heritage from the original Berkeley RocketChip implementation.
- Andes Technology found in designs such as MicroChips’s PolarFire SoC.
As RISC-V is an open architecture there are many other options.
- Professionally my experience has been with IQonIC Works RISC-V IP. They provide a small RV32EC core targeted at deeply embedded applications, where stripped-down bare-metal programming is essential.
- Many open-source options.
A complete list is maintained by the RISC-V org.